package com.sunday.common.statemachine.study.e5_Junction;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.state.State;

/**
 * Junction:
 * 语义：junction 是一种汇聚状态，用于将多个可能的路径合并为一个。
 * 行为：没有条件逻辑，仅仅作为一个汇聚点，通常会自动流转到下一个状态。
 * 使用场景：当多个状态需要合并到一个状态时使用。
 * 示例：多个订单状态最终都可以汇聚到 SHIPPED 状态。
 * Choice:
 * 语义：choice 是一种条件决策状态，Spring 状态机会根据条件（通常是 Guard）来判断接下来应该转到哪个状态。
 * 行为：基于上下文中的条件来选择不同的目标状态。
 * 使用场景：当状态转换需要基于某些条件进行选择时使用。
 * 示例：根据库存状态判断是进入支付流程还是库存回冲。
 */

@Slf4j
@Configuration  // 声明该类为Spring的配置类
@EnableStateMachine  // 启用Spring状态机功能
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStates, OrderEvents> {

    @Override
    public void configure(StateMachineConfigurationConfigurer<OrderStates, OrderEvents> config) throws Exception {
        config
                .withConfiguration()
                .autoStartup(true)
                .listener(listener());
    }

    @Override
    public void configure(StateMachineStateConfigurer<OrderStates, OrderEvents> states) throws Exception {
        states
                .withStates()
                .initial(OrderStates.NEW)
                .state(OrderStates.CHECKED)
                .state(OrderStates.BACKWASH)
                .junction(OrderStates.UNPAID)
                .state(OrderStates.PAID)
                .state(OrderStates.SHIPPED)
                .end(OrderStates.COMPLETED)
                .end(OrderStates.CANCELED)
//                .states(EnumSet.allOf(OrderStates.class))
                .and()  // 结束当前状态定义，开始下一个状态规则
        ;

    }

    @Override
    public void configure(StateMachineTransitionConfigurer<OrderStates, OrderEvents> transitions) throws Exception {
        transitions
                // 新建订单 -> 检查库存 触发条件 检查库存事件
                .withExternal()  // 配置外部状态转换（即不同状态之间的转换）
                .source(OrderStates.NEW)
                .target(OrderStates.CHECKED)
                .event(OrderEvents.CHECK_STOCK)
                .action(context -> {
                    log.info("CHECKED=== {}", context.getStateMachine().getState().getId());
                })
                .and()  // 结束当前转换定义，开始下一个转换规则

                .withExternal()
                .source(OrderStates.CHECKED)
                .target(OrderStates.UNPAID)
                .event(OrderEvents.OUTBOUND)
                .action(context -> {
                    log.info("UNPAID=== {}", context.getStateMachine().getState().getId());
                })
                .and()  // 结束当前转换定义，开始下一个转换规则

                .withExternal()
                .source(OrderStates.NEW)
                .target(OrderStates.UNPAID)
                .event(OrderEvents.SHIP)
                .action(context -> {
                    log.info("BACKWASH=== {}", context.getStateMachine().getState().getId());
                })
                .and()  // 结束当前转换定义，开始下一个转换规则

                /*
                 * 再次必须引用一下官方文档中的描述问题 已区分 choice 和 junction
                 * https://docs.spring.io/spring-statemachine/docs/4.0.0/reference/index.html#statemachine-config-states-junction
                 * 选择和交汇点之间的区别纯粹是学术性的，因为两者都是 使用 Structures 实现。
                 * 然而，从理论上讲，基于 在 UML 建模中，只允许一个传入转换，而允许多个传入转换。在代码级别， 功能几乎相同。
                 * first/then/last choice junction
                 */
                .withJunction()
                .source(OrderStates.UNPAID)  // 从多个状态汇聚
                .first(OrderStates.PAID,
                        context -> {
                            /*
                             * 当前状态一定要注意，如这个例子中 NEW -> CHECKED -> UNPAID -> PAID 的时候，因为本身方法验证完成后
                             * 并没有进行有效的状态切换，所以当前的状态获取应该是 CHECKED
                             */
                            log.info("当前状态打印 : {}", context.getStateMachine().getState());
                            boolean flag = context.getStateMachine().getState().getId() == OrderStates.CHECKED;
                            log.info("是否为 PAID 操作：{}", flag);
                            return flag;
                        },
                        context -> {
                            log.info("PAID=== {}", context.getStateMachine().getState().getId());
                        })
                .last(OrderStates.SHIPPED, context -> {
                    log.info("SHIPPED=== {}", context.getStateMachine().getState().getId());
                })
                .and()

//                .withExternal()
//                .source(OrderStates.PAID)
//                .target(OrderStates.SHIPPED)
//                .event(OrderEvents.SHIP)
//                .action(context -> {
//                    log.info("SHIPPED=== {}", context.getStateMachine().getState().getId());
//                })
//                .and()
//
//                .withExternal()
//                .source(OrderStates.SHIPPED)
//                .target(OrderStates.COMPLETED)
//                .event(OrderEvents.COMPLETE)
//                .action(context -> {
//                    log.info("COMPLETED=== {}", context.getStateMachine().getState().getId());
//                })
//                .and()
        ;
    }


    /**
     * 为什么触发两次 stateChanged()？
     * <p>
     * 因为状态机中存在层次结构，从 UNPAID 到 PAID 是一次状态变更，而从 PAID 自动切换到 PENDING_OUTBOUND 是另一层次变更。
     * 为什么 UNPAID to PAID 出现在 PENDING_OUTBOUND 后？
     * <p>
     * 日志记录的顺序是由状态进入的顺序决定的：先进入 PAID，再进入其子状态 PENDING_OUTBOUND。但 PENDING_OUTBOUND 是最终的细化状态，因此先记录。
     * 如何解决？
     * <p>
     * 在 stateChanged() 方法中通过 getParent() 方法提取父状态，记录完整的父子状态信息。
     * 也可以考虑使用 entry() 和 exit() 钩子方法进行状态进入和退出日志的更精确记录。
     */
    @Bean
    public StateMachineListener<OrderStates, OrderEvents> listener() {
        return new StateMachineListenerAdapter<>() {
            @Override
            public void stateChanged(State<OrderStates, OrderEvents> from, State<OrderStates, OrderEvents> to) {
                log.info("State change from {} to {}", from == null ? "无" : from.getId(), to.getId());
            }
        };
    }
}